from time import sleep
from typing import List
import keyboard
import mido

KEYS = (
    ('z', 'x', 'c', 'v', 'b', 'n', 'm'),
    ('a', 's', 'd', 'f', 'g', 'h', 'j'),
    ('q', 'w', 'e', 'r', 't', 'y', 'u'),
)
# lowest_do_note(z) = 48, medium_do_note(a) = 60, highest_do_note(z) = 72
KEY_MAP = {
    72: 'q', 74: 'w', 76: 'e', 77: 'r', 79: 't', 81: 'y', 83: 'u',
    60: 'a', 62: 's', 64: 'd', 65: 'f', 67: 'g', 69: 'h', 71: 'j',
    48: 'z', 50: 'x', 52: 'c', 53: 'v', 55: 'b', 57: 'n', 59: 'm',
}
# fill unplayable note key map with the upper note key
for note in range(48, 84):
    if note not in KEY_MAP:
        KEY_MAP[note] = KEY_MAP[note+1]

def note2key(note: int) -> str:
    if note < 48 or note >= 84:
        return None
    return KEY_MAP[note]

NORMAL_NOTE_TYPE=0
EMPTY_NOTE_TYPE=1

class Msg:
    type = NORMAL_NOTE_TYPE
    time = 0
    key = ''

    def __init__(self, type, time, key='') -> None:
        self.type = type
        self.time = time
        self.key = key

    def __str__(self) -> str:
        if self.type == NORMAL_NOTE_TYPE:
            return f'{self.key} {self.time}'
        return f'EMPTY {self.time}'

class MidiPlayer:
    speed = 100
    sleep_scale = 1.0
    pause = False

    def set_speed(self, speed):
        self.speed = speed
        self.sleep_scale = round(100 / self.speed, 3)

    def toggle_pause(self):
        self.pause = not self.pause

    def output_midi_msg_to_file(self, midi: mido.MidiFile, filename: str):
        out = open(filename, 'w+')
        count = 0
        for msg in midi:
            count += 1
            if msg.is_meta:
                continue
            print(count, msg, file=out)
        out.close()

    def output_msg_to_file(self, messages: List[Msg], filename: str):
        out = open(filename, 'w+')
        count = 0
        for message in messages:
            count += 1
            print(count, message, file=out)
        out.close()

    def extract_playable_msg(self, midi: mido.MidiFile) -> List[Msg]:
        messages = []
        for msg in midi:
            if msg.is_meta:
                continue
            if msg.type == "control_change":
                if msg.time > 0:
                    message = Msg(EMPTY_NOTE_TYPE, msg.time)
                    messages.append(message)
                continue
            if msg.type == "note_on":
                key = note2key(msg.note)
                if key is None or msg.velocity == 0:
                    if msg.time > 0:
                        message = Msg(EMPTY_NOTE_TYPE, msg.time)
                        messages.append(message)
                    continue
                message = Msg(NORMAL_NOTE_TYPE, msg.time, key)
                messages.append(message)
        return messages
                
    def combine_messages(self, messages: List[Msg]) -> List[Msg]:
        new_messages = []
        for msg in messages:
            if len(new_messages) == 0:
                new_messages.append(msg)
                continue
            if new_messages[-1].type == EMPTY_NOTE_TYPE:
                # substitute last message
                msg.time += new_messages[-1].time
                new_messages[-1] = msg
                continue
            if msg.type == EMPTY_NOTE_TYPE:
                new_messages.append(msg)
                continue
            if msg.time > 0:    # the start note
                new_messages.append(msg)
            else:               # the composition note
                new_messages[-1].key += f'+{msg.key}'
        return new_messages

    def play_messages(self, messages: List[Msg]):
        for message in messages:
            while self.pause:
                sleep(1)
            if message.time > 0:
                sleep(message.time * self.sleep_scale)
            if message.type == EMPTY_NOTE_TYPE:
                continue
            # print(message['key'])
            keyboard.press_and_release(message.key)
            # self.keyboard.press(message['key'])
            # self.keyboard.release(message['key'])

    def play(self, filename):
        midi = mido.MidiFile(filename, clip=True)
        self.output_midi_msg_to_file(midi, 'msg.txt')
        messages = self.extract_playable_msg(midi)
        messages = self.combine_messages(messages)
        self.output_msg_to_file(messages, 'msg_keys.txt')

        print('play in 2 seconds:', filename)
        sleep(2)
        self.play_messages(messages)
        print('end.')
